﻿using ImageK.Gui;
using ImageK.IO;
using ImageK.Java;
using ImageK;
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.IO.Packaging;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Channels;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
using static System.Windows.Forms.Design.AxImporter;
using System.Xml.Linq;

namespace ImageK.IO
{
    /** This class decodes an ImageJ .roi file. 
        <p>
        Format of the original 64 byte ImageJ/NIH Image
        .roi file header. Two byte numbers are big-endian
        signed shorts. The JavaScript example at
        http://wsr.imagej.net/macros/js/DecodeRoiFile.js
        demonstrates how to use this information to 
        decode a .roi file.
        <pre>
        0-3		"Iout"
        4-5		version (>=217)
        6-7		roi type (encoded as one byte)
        8-9		top
        10-11	left
        12-13	bottom
        14-15	right
        16-17	NCoordinates
        18-33	x1,y1,x2,y2 (straight line) | x,y,width,height (double rect) | size (npoints)
        34-35	stroke width (v1.43i or later)
        36-39   ShapeRoi size (type must be 1 if this value>0)
        40-43   stroke color (v1.43i or later)
        44-47   fill color (v1.43i or later)
        48-49   subtype (v1.43k or later)
        50-51   options (v1.43k or later)
        52-52   arrow style or aspect ratio (v1.43p or later)
        53-53   arrow head size (v1.43p or later)
        54-55   rounded rect arc size (v1.43p or later)
        56-59   position
        60-63   header2 offset
        64-       x-coordinates (short), followed by y-coordinates
        <pre>
        @see <a href="http://wsr.imagej.net/macros/js/DecodeRoiFile.js">DecodeRoiFile.js</a>
    */

    public class RoiDecoder
    {
        // offsets
        public static int VERSION_OFFSET = 4;
        public static int TYPE = 6;
        public static int TOP = 8;
        public static int LEFT = 10;
        public static int BOTTOM = 12;
        public static int RIGHT = 14;
        public static int N_COORDINATES = 16;
        public static int X1 = 18;
        public static int Y1 = 22;
        public static int X2 = 26;
        public static int Y2 = 30;
        public static int XD = 18;
        public static int YD = 22;
        public static int WIDTHD = 26;
        public static int HEIGHTD = 30;
        public static int SIZE = 18;
        public static int STROKE_WIDTH = 34;
        public static int SHAPE_ROI_SIZE = 36;
        public static int STROKE_COLOR = 40;
        public static int FILL_COLOR = 44;
        public static int SUBTYPE = 48;
        public static int OPTIONS = 50;
        public static int ARROW_STYLE = 52;
        public static int FLOAT_PARAM = 52; //ellipse ratio or rotated rect width
        public static int POINT_TYPE = 52;
        public static int ARROW_HEAD_SIZE = 53;
        public static int ROUNDED_RECT_ARC_SIZE = 54;
        public static int POSITION = 56;
        public static int HEADER2_OFFSET = 60;
        public static int COORDINATES = 64;
        // header2 offsets
        public static int C_POSITION = 4;
        public static int Z_POSITION = 8;
        public static int T_POSITION = 12;
        public static int NAME_OFFSET = 16;
        public static int NAME_LENGTH = 20;
        public static int OVERLAY_LABEL_COLOR = 24;
        public static int OVERLAY_FONT_SIZE = 28; //short
        public static int GROUP = 30;  //byte
        public static int IMAGE_OPACITY = 31;  //byte
        public static int IMAGE_SIZE = 32;  //int
        public static int FLOAT_STROKE_WIDTH = 36;  //float
        public static int ROI_PROPS_OFFSET = 40;
        public static int ROI_PROPS_LENGTH = 44;
        public static int COUNTERS_OFFSET = 48;

        // subtypes
        public static int TEXT = 1;
        public static int ARROW = 2;
        public static int ELLIPSE = 3;
        public static int IMAGE = 4;
        public static int ROTATED_RECT = 5;

        // options
        public static int SPLINE_FIT = 1;
        public static int DOUBLE_HEADED = 2;
        public static int OUTLINE = 4;
        public static int OVERLAY_LABELS = 8;
        public static int OVERLAY_NAMES = 16;
        public static int OVERLAY_BACKGROUNDS = 32;
        public static int OVERLAY_BOLD = 64;
        public static int SUB_PIXEL_RESOLUTION = 128;
        public static int DRAW_OFFSET = 256;
        public static int ZERO_TRANSPARENT = 512;
        public static int SHOW_LABELS = 1024;
        public static int SCALE_LABELS = 2048;
        public static int PROMPT_BEFORE_DELETING = 4096; //points
        public static int SCALE_STROKE_WIDTH = 8192;

        // types
        private const int polygon = 0, rect = 1, oval = 2, line = 3, freeline = 4, polyline = 5, noRoi = 6,
            freehand = 7, traced = 8, angle = 9, point = 10;

        private byte[] data;
        private string path;
        private Stream ins;
	    private string name;
        private int size;

        /** Constructs an RoiDecoder using a file path. */
        public RoiDecoder(string path)
        {
            this.path = path;
        }

        /** Constructs an RoiDecoder using a byte array. */
        public RoiDecoder(byte[] bytes, string name)
        {

            ins = new MemoryStream(bytes);
            this.name = name;
            this.size = bytes.Length;
        }

        /** Opens the Roi at the specified path. Returns null if there is an error. */
        public static Roi open(String path)
        {
            Roi roi = null;
            RoiDecoder rd = new RoiDecoder(path);
            try
            {
                roi = rd.getRoi();
            }
            catch (IOException e) { }
            return roi;
        }

        /** Returns the ROI. */
        public Roi getRoi()
        {
		    if (path!=null) {
			    System.IO.FileInfo f = new System.IO.FileInfo(path);
                size = (int) f.Length;
			    if (!path.EndsWith(".roi") && size>5242880)
				    throw new IOException("This is not an ROI or file size>5MB)");
                name = f.Name;
			    ins = new FileStream(path, FileMode.Open);
            }
            data = new byte[size];

		    int total = 0;
		    while (total<size)
                total += ins.Read(data, total, size-total);
		    ins.Close();
		    if (getByte(0)!=73 || getByte(1)!=111)  //"Iout"
			    throw new IOException("This is not an ImageJ ROI");
            int version = getShort(VERSION_OFFSET);
            int type = getByte(TYPE);
            int subtype = getShort(SUBTYPE);
            int top = getShort(TOP);
            int left = getShort(LEFT);
            int bottom = getShort(BOTTOM);
            int right = getShort(RIGHT);
            int width = right-left;
            int height = bottom-top;
            int n = getUnsignedShort(N_COORDINATES);
		    if (n==0)
			    n = getInt(SIZE);
            int options = getShort(OPTIONS);
            int position = getInt(POSITION);
            int hdr2Offset = getInt(HEADER2_OFFSET);
            int channel = 0, slice = 0, frame = 0;
            int overlayLabelColor = 0;
            int overlayFontSize = 0;
            int group = 0;
            int imageOpacity = 0;
            int imageSize = 0;
            bool subPixelResolution = (options&SUB_PIXEL_RESOLUTION)!=0 &&  version>=222;
            bool drawOffset = subPixelResolution && (options&DRAW_OFFSET)!=0;
            bool scaleStrokeWidth = true;
		    if (version>=228)
			    scaleStrokeWidth = (options&SCALE_STROKE_WIDTH)!=0;
		    
		    bool subPixelRect = version>=223 && subPixelResolution && (type==rect||type==oval);
            double xd = 0.0, yd = 0.0, widthd = 0.0, heightd = 0.0;
		    if (subPixelRect) {
			    xd = getFloat(XD);
                yd = getFloat(YD);
                widthd = getFloat(WIDTHD);
                heightd = getFloat(HEIGHTD);
            }

            if (hdr2Offset>0 && hdr2Offset+IMAGE_SIZE+4<=size)
            {
                channel = getInt(hdr2Offset+C_POSITION);
                slice = getInt(hdr2Offset+Z_POSITION);
                frame = getInt(hdr2Offset+T_POSITION);
                overlayLabelColor = getInt(hdr2Offset+OVERLAY_LABEL_COLOR);
                overlayFontSize = getShort(hdr2Offset+OVERLAY_FONT_SIZE);
                imageOpacity = getByte(hdr2Offset+IMAGE_OPACITY);
                imageSize = getInt(hdr2Offset+IMAGE_SIZE);
                group = getByte(hdr2Offset+GROUP);
            }

            if (name!=null && name.EndsWith(".roi"))
                name = name.Substring(0, name.Length-4);
            bool isComposite = getInt(SHAPE_ROI_SIZE)>0;

            Roi roi = null;
            if (isComposite)
            {
                roi = getShapeRoi();
                if (version>=218)
                    getStrokeWidthAndColor(roi, hdr2Offset, scaleStrokeWidth);
                roi.setPosition(position);
                if (channel>0 || slice>0 || frame>0)
                    roi.setPosition(channel, slice, frame);
                decodeOverlayOptions(roi, version, options, overlayLabelColor, overlayFontSize);
                if (version>=224)
                {
                    string props = getRoiProps();
                    if (props!=null)
                        roi.setProperties(props);
                }
                if (version>=228 && group>0)
                    roi.setGroup(group);
                return roi;
            }

            switch (type)
            {
                case rect:
                    if (subPixelRect)
                        roi = new Roi(xd, yd, widthd, heightd);
                    else
                        roi = new Roi(left, top, width, height);
                    int arcSize = getShort(ROUNDED_RECT_ARC_SIZE);
                    if (arcSize>0)
                        roi.setCornerDiameter(arcSize);
                    break;
                case oval:
                    if (subPixelRect)
                        roi = new OvalRoi(xd, yd, widthd, heightd);
                    else
                        roi = new OvalRoi(left, top, width, height);
                    break;
                case line:
                    double x1 = getFloat(X1);
                    double y1 = getFloat(Y1);
                    double x2 = getFloat(X2);
                    double y2 = getFloat(Y2);
                    //todo:
                    // if (subtype==ARROW)
                    // {
                    //     roi = new Arrow(x1, y1, x2, y2);
                    //     ((Arrow)roi).setDoubleHeaded((options&DOUBLE_HEADED)!=0);
                    //     ((Arrow)roi).setOutline((options&OUTLINE)!=0);
                    //     int style = getByte(ARROW_STYLE);
                    //     if (style>=Arrow.FILLED && style<=Arrow.BAR)
                    //         ((Arrow)roi).setStyle(style);
                    //     int headSize = getByte(ARROW_HEAD_SIZE);
                    //     if (headSize>=0 && style<=30)
                    //         ((Arrow)roi).setHeadSize(headSize);
                    // }
                    // else
                    // {
                    //     roi = new Line(x1, y1, x2, y2);
                    //     roi.setDrawOffset(drawOffset);
                    // }
                    break;
                case polygon:
                case freehand:
                case traced:
                case polyline:
                case freeline:
                case angle:
                case point:
                    //IJ.log("type: "+type);
                    //IJ.log("n: "+n);
                    //ij.IJ.log("rect: "+left+","+top+" "+width+" "+height);
                    if (n==0 || n<0) break;
                    int[] x = new int[n];
                    int[] y = new int[n];
                    float[] xf = null;
                    float[] yf = null;
                    int base1 = COORDINATES;
                    int base2 = base1+2*n;
                    int xtmp, ytmp;
                    for (int i = 0; i<n; i++)
                    {
                        xtmp = getShort(base1+i*2);
                        if (xtmp<0) xtmp = 0;
                        ytmp = getShort(base2+i*2);
                        if (ytmp<0) ytmp = 0;
                        x[i] = left+xtmp;
                        y[i] = top+ytmp;
                    }
                    if (subPixelResolution)
                    {
                        xf = new float[n];
                        yf = new float[n];
                        base1 = COORDINATES+4*n;
                        base2 = base1+4*n;
                        for (int i = 0; i<n; i++)
                        {
                            xf[i] = getFloat(base1+i*4);
                            yf[i] = getFloat(base2+i*4);
                        }
                    }
                    if (type==point)
                    {
                        if (subPixelResolution)
                            roi = new PointRoi(xf, yf, n);
                        else
                            roi = new PointRoi(x, y, n);
                        if (version>=226)
                        {
                            ((PointRoi)roi).setPointType(getByte(POINT_TYPE));
                            ((PointRoi)roi).setSize(getShort(STROKE_WIDTH));
                        }
                        if ((options&SHOW_LABELS)!=0 && !Prefs.noPointLabels)
                            ((PointRoi)roi).setShowLabels(true);
                        if ((options&PROMPT_BEFORE_DELETING)!=0)
                            ((PointRoi)roi).promptBeforeDeleting(true);
                        break;
                    }
                    int roiType;
                    if (type==polygon)
                        roiType = Roi.POLYGON;
                    else if (type==freehand)
                    {
                        roiType = Roi.FREEROI;
                        if (subtype==ELLIPSE || subtype==ROTATED_RECT)
                        {
                            double ex1 = getFloat(X1);
                            double ey1 = getFloat(Y1);
                            double ex2 = getFloat(X2);
                            double ey2 = getFloat(Y2);
                            double param = getFloat(FLOAT_PARAM);
                            if (subtype==ROTATED_RECT)
                                roi = new RotatedRectRoi(ex1, ey1, ex2, ey2, param);
                            else
                                roi = new EllipseRoi(ex1, ey1, ex2, ey2, param);
                            break;
                        }
                    }
                    else if (type==traced)
                        roiType = Roi.TRACED_ROI;
                    else if (type==polyline)
                        roiType = Roi.POLYLINE;
                    else if (type==freeline)
                        roiType = Roi.FREELINE;
                    else if (type==angle)
                        roiType = Roi.ANGLE;
                    else
                        roiType = Roi.FREEROI;
                    if (subPixelResolution)
                    {
                        roi = new PolygonRoi(xf, yf, n, roiType);
                        roi.setDrawOffset(drawOffset);
                    }
                    else
                        roi = new PolygonRoi(x, y, n, roiType);
                    break;
                default:
                    throw new IOException("Unrecognized ROI type: "+type);
            }
            if (roi==null)
                return null;
            roi.setName(getRoiName());

            // read stroke width, stroke color and fill color (1.43i or later)
            if (version>=218)
            {
                getStrokeWidthAndColor(roi, hdr2Offset, scaleStrokeWidth);
                if (type==point)
                    roi.setStrokeWidth(0);
                bool splineFit = (options&SPLINE_FIT)!=0;
                if (splineFit && roi is PolygonRoi)
				            ((PolygonRoi)roi).fitSpline();
            }

            if (version>=218 && subtype==TEXT)
                roi = getTextRoi(roi, version);

            if (version>=221 && subtype==IMAGE)
                roi = getImageRoi(roi, imageOpacity, imageSize, options);

            if (version>=224)
            {
                String props = getRoiProps();
                if (props!=null)
                    roi.setProperties(props);
            }

            if (version>=227)
            {
                int[] counters = getPointCounters(n);
                if (counters!=null && (roi is PointRoi))
				            ((PointRoi)roi).setCounters(counters);
            }

            // set group (1.52t or later)
            if (version>=228 && group>0)
                roi.setGroup(group);

            roi.setPosition(position);
            if (channel>0 || slice>0 || frame>0)
                roi.setPosition(channel, slice, frame);
            decodeOverlayOptions(roi, version, options, overlayLabelColor, overlayFontSize);
            return roi;
	    }
	
        void decodeOverlayOptions(Roi roi, int version, int options, int color, int fontSize)
        {
            //todo:
            // Overlay proto = new Overlay();
            // proto.drawLabels((options&OVERLAY_LABELS)!=0);
            // proto.drawNames((options&OVERLAY_NAMES)!=0);
            // proto.drawBackgrounds((options&OVERLAY_BACKGROUNDS)!=0);
            // if (version>=220 && color!=0)
            //     proto.setLabelColor(new Color(color));
            // bool bold = (options&OVERLAY_BOLD)!=0;
            // bool scalable = (options&SCALE_LABELS)!=0;
            // if (fontSize>0 || bold || scalable)
            // {
            //     proto.setLabelFont(new Font("SansSerif", bold ? FontStyle.Bold : FontStyle.Regular, fontSize), scalable);
            // }
            // roi.setPrototypeOverlay(proto);
        }

        void getStrokeWidthAndColor(Roi roi, int hdr2Offset, bool scaleStrokeWidth)
        {
            double strokeWidth = getShort(STROKE_WIDTH);
            if (hdr2Offset>0)
            {
                double strokeWidthD = getFloat(hdr2Offset+FLOAT_STROKE_WIDTH);
                if (strokeWidthD>0.0)
                    strokeWidth = strokeWidthD;
            }
            if (strokeWidth>0.0)
            {
                if (scaleStrokeWidth)
                    roi.setStrokeWidth(strokeWidth);
                else
                    roi.setUnscalableStrokeWidth(strokeWidth);
            }
            int strokeColor = getInt(STROKE_COLOR);
            if (strokeColor!=0)
            {
                int alpha = (strokeColor>>24)&0xff;
                roi.setStrokeColor(Color.FromArgb(strokeColor));
            }
            int fillColor = getInt(FILL_COLOR);
            if (fillColor!=0)
            {
                int alpha = (fillColor>>24)&0xff;
                roi.setFillColor(Color.FromArgb(fillColor));
            }
        }

        public Roi getShapeRoi()
        {
		    int type = getByte(TYPE);
		    if (type!=rect)
			    throw new ArgumentException("Invalid composite ROI type");
            int top = getShort(TOP);
            int left = getShort(LEFT);
            int bottom = getShort(BOTTOM);
            int right = getShort(RIGHT);
            int width = right-left;
            int height = bottom-top;
            int n = getInt(SHAPE_ROI_SIZE);

            ShapeRoi roi = null;
            float[] shapeArray = new float[n];
            int bas = COORDINATES;
            for (int i = 0; i<n; i++)
            {
                shapeArray[i] = getFloat(bas);
                bas += 4;
            }
            roi = new ShapeRoi(shapeArray);
            roi.setName(getRoiName());
            return roi;
        }
	
        Roi getTextRoi(Roi roi, int version)
        {
            Rectangle r = roi.getBounds();
            int hdrSize = RoiEncoder.HEADER_SIZE;
            int size = getInt(hdrSize);
            int styleAndJustification = getInt(hdrSize+4);
            int style = styleAndJustification&255;
            int justification = (styleAndJustification>>8) & 3;
            bool drawStringMode = (styleAndJustification&1024)!=0;
            int nameLength = getInt(hdrSize+8);
            int textLength = getInt(hdrSize+12);
            char[] name = new char[nameLength];
            char[] text = new char[textLength];
            for (int i = 0; i<nameLength; i++)
                name[i] = (char)getShort(hdrSize+16+i*2);
            for (int i = 0; i<textLength; i++)
                text[i] = (char)getShort(hdrSize+16+nameLength*2+i*2);
            double angle = version>=225 ? getFloat(hdrSize+16+nameLength*2+textLength*2) : 0f;
            Font font = new Font(new FontFamily(new string(name)), (float)(size), FontStyle.Regular);
            TextRoi roi2 = null;
            if (roi.subPixelResolution())
            {
                RectangleF fb = roi.getFloatBounds();
                roi2 = new TextRoi(fb.X, fb.Y, fb.Width, fb.Height, new String(text), font);
            }
            else
                roi2 = new TextRoi(r.X, r.Y, r.Width, r.Height, new String(text), font);
            roi2.setStrokeColor(roi.getStrokeColor());
            roi2.setFillColor(roi.getFillColor());
            roi2.setName(getRoiName());
            roi2.setJustification(justification);
            roi2.setDrawStringMode(drawStringMode);
            roi2.setAngle(angle);
            return roi2;
        }

        Roi getImageRoi(Roi roi, int opacity, int size, int options)
        {
            throw new NotImplementedException();
            // if (size<=0)
            //     return roi;
            // Rectangle r = roi.getBounds();
            // byte[] bytes = new byte[size];
            // for (int i = 0; i<size; i++)
            //     bytes[i] = (byte)getByte(COORDINATES+i);
            // ImagePlus imp = new Opener().deserialize(bytes);
            // ImageRoi roi2 = new ImageRoi(r.x, r.y, imp.getProcessor());
            // roi2.setOpacity(opacity/255.0);
            // if ((options&ZERO_TRANSPARENT)!=0)
            //     roi2.setZeroTransparent(true);
            // return roi2;
        }

        string getRoiName()
        {
            string fileName = name;
            int hdr2Offset = getInt(HEADER2_OFFSET);
            if (hdr2Offset==0)
                return fileName;
            int offset = getInt(hdr2Offset+NAME_OFFSET);
            int length = getInt(hdr2Offset+NAME_LENGTH);
            if (offset==0 || length==0)
                return fileName;
            if (offset+length*2>size)
                return fileName;
            char[] nam = new char[length];
            for (int i = 0; i<length; i++)
                nam[i] = (char)getShort(offset+i*2);
            return new string(nam);
        }

        string getRoiProps()
        {
            int hdr2Offset = getInt(HEADER2_OFFSET);
            if (hdr2Offset==0)
                return null;
            int offset = getInt(hdr2Offset+ROI_PROPS_OFFSET);
            int length = getInt(hdr2Offset+ROI_PROPS_LENGTH);
            if (offset==0 || length==0)
                return null;
            if (offset+length*2>size)
                return null;
            char[] props = new char[length];
            for (int i = 0; i<length; i++)
                props[i] = (char)getShort(offset+i*2);
            return new String(props);
        }

        int[] getPointCounters(int n)
        {
            int hdr2Offset = getInt(HEADER2_OFFSET);
            if (hdr2Offset==0)
                return null;
            int offset = getInt(hdr2Offset+COUNTERS_OFFSET);
            if (offset==0)
                return null;
            if (offset+n*4>data.Length)
                return null;
            int[] counters = new int[n];
            for (int i = 0; i<n; i++)
                counters[i] = getInt(offset+i*4);
            return counters;
        }


        int getByte(int bas)
        {
            return data[bas]&255;
        }

        int getShort(int bas)
        {
            int b0 = data[bas]&255;
            int b1 = data[bas+1]&255;
            int n = (short)((b0<<8) + b1);
            if (n<-5000)
                n = (b0<<8) + b1; // assume n>32767 and unsigned
            return n;
        }

        int getUnsignedShort(int bas)
        {
            int b0 = data[bas]&255;
            int b1 = data[bas+1]&255;
            return (b0<<8) + b1;
        }

        int getInt(int bas)
        {
            int b0 = data[bas]&255;
            int b1 = data[bas+1]&255;
            int b2 = data[bas+2]&255;
            int b3 = data[bas+3]&255;
            return ((b0<<24) + (b1<<16) + (b2<<8) + b3);
        }

        float getFloat(int bas)
        {
            return BitConverter.Int32BitsToSingle(getInt(bas));
            // return Float.intBitsToFloat(getInt(bas));
        }

        /** Opens an ROI from a byte array. */
        public static Roi openFromByteArray(byte[] bytes)
        {
            Roi roi = null;
            if (bytes==null || bytes.Length==0)
                return roi;
            try
            {
                RoiDecoder decoder = new RoiDecoder(bytes, null);
                roi = decoder.getRoi();
            }
            catch (IOException e)
            {
                return null;
            }
            return roi;
        }

}

}
